import shimmer from './shimmer'

const wrap = shimmer.wrap
const unwrap = shimmer.unwrap

// Default to complaining loudly when things don't go according to plan.
// dunderscores are boring
const SYMBOL = 'wrap@before'

// Sets a property on an object, preserving its enumerability.
// This function assumes that the property is already writable.
function defineProperty(obj, name, value) {
  const enumerable = !!obj[name] && obj.propertyIsEnumerable(name)
  Object.defineProperty(obj, name, {
    configurable: true,
    enumerable: enumerable,
    writable: true,
    value: value
  })
}

function _process(self, listeners) {
  const l = listeners.length
  for(var p = 0; p < l; p++) {
    var listener = listeners[p]
    // set up the listener so that onEmit can do whatever it needs
    var before = self[SYMBOL]
    if(typeof before === 'function') {
      before(listener)
    } else if(Array.isArray(before)) {
      var length = before.length
      for(var i = 0; i < length; i++) before[i](listener)
    }
  }
}

function _listeners(self, event) {
  var listeners
  listeners = self._events && self._events[event]
  if(!Array.isArray(listeners)) {
    if(listeners) {
      listeners = [listeners]
    } else {
      listeners = []
    }
  }

  return listeners
}

function _findAndProcess(self, event, before) {
  var after = _listeners(self, event)
  var unprocessed = after.filter(function(fn) { return before.indexOf(fn) === -1 })
  if(unprocessed.length > 0) _process(self, unprocessed)
}

function _wrap(unwrapped, visit) {
  if(!unwrapped) return

  var wrapped = unwrapped
  if(typeof unwrapped === 'function') {
    wrapped = visit(unwrapped)
  } else if(Array.isArray(unwrapped)) {
    wrapped = []
    for(var i = 0; i < unwrapped.length; i++) {
      wrapped[i] = visit(unwrapped[i])
    }
  }
  return wrapped
}

export default function wrapEmitter(emitter, onAddListener, onEmit) {
  if(!emitter || !emitter.on || !emitter.addListener ||
    !emitter.removeListener || !emitter.emit) {
    throw new Error('can only wrap real EEs')
  }

  if(!onAddListener) throw new Error('must have function to run on listener addition')
  if(!onEmit) throw new Error('must have function to wrap listeners when emitting')

  /* Attach a context to a listener, and make sure that this hook stays
   * attached to the emitter forevermore.
   */
  function adding(on) {
    return function added(event, listener) {
      var existing = _listeners(this, event).slice()

      try {
        var returned = on.call(this, event, listener)
        _findAndProcess(this, event, existing)
        return returned
      } finally {
        // old-style streaming overwrites .on and .addListener, so rewrap
        if(!this.on.__wrapped) wrap(this, 'on', adding)
        if(!this.addListener.__wrapped) wrap(this, 'addListener', adding)
      }
    }
  }

  function emitting(emit) {
    return function emitted(event) {
      if(!this._events || !this._events[event]) return emit.apply(this, arguments)

      var unwrapped = this._events[event]

      /* Ensure that if removeListener gets called, it's working with the
       * unwrapped listeners.
       */
      function remover(removeListener) {
        return function removed() {
          this._events[event] = unwrapped
          try {
            return removeListener.apply(this, arguments)
          } finally {
            unwrapped = this._events[event]
            this._events[event] = _wrap(unwrapped, onEmit)
          }
        }
      }

      wrap(this, 'removeListener', remover)

      try {
        /* At emit time, ensure that whatever else is going on, removeListener will
         * still work while at the same time running whatever hooks are necessary to
         * make sure the listener is run in the correct context.
         */
        this._events[event] = _wrap(unwrapped, onEmit)
        return emit.apply(this, arguments)
      } finally {
        /* Ensure that regardless of what happens when preparing and running the
         * listeners, the status quo ante is restored before continuing.
         */
        unwrap(this, 'removeListener')
        this._events[event] = unwrapped
      }
    }
  }

  // support multiple onAddListeners
  if(!emitter[SYMBOL]) {
    defineProperty(emitter, SYMBOL, onAddListener)
  } else if(typeof emitter[SYMBOL] === 'function') {
    defineProperty(emitter, SYMBOL, [emitter[SYMBOL], onAddListener])
  } else if(Array.isArray(emitter[SYMBOL])) {
    emitter[SYMBOL].push(onAddListener)
  }

  // only wrap the core functions once
  if(!emitter.__wrapped) {
    wrap(emitter, 'addListener', adding)
    wrap(emitter, 'on', adding)
    wrap(emitter, 'emit', emitting)

    defineProperty(emitter, '__unwrap', function() {
      unwrap(emitter, 'addListener')
      unwrap(emitter, 'on')
      unwrap(emitter, 'emit')
      delete emitter[SYMBOL]
      delete emitter.__wrapped
    })
    defineProperty(emitter, '__wrapped', true)
  }
}
